Skip to main content

Inference logs and proofs

Z3 version 4.12.0 exposes new functionality to capture inferences. There is an API extension to register a callback that is invoked whenever the main SMT engine infers a clause. It is also possible to save inferred clauses together with proof hints that justify them. We use the terminology hint because the justifications are big step inferences. Some of the steps can be checked by lean self-contained proof checkers, other steps do not contain detailed guidance that would allow efficient validation. They require checking using general purpose SMT solving.

Proof terms have been supported from Z3 since 2008. They have been used in various settings, including for replaying tactics in Isabelle, for generation of interpolants, and for extracting separating hyper-planes from linear programming proofs (Farkas lemma). A separate format is used to trace inference steps for proof mining. Proof logs allow to simplify some of the infrastructure around proof reconstruction during search and for proof mining. The same logs can be targeted for the different use cases. The survey ProofsForSMT discusses proof formats in use by SMT solvers and includes some additional technical details on proof logs.

Callbacks for clause inferences

The API exposes a function Z3_solver_register_on_clause that is first exposed for C, C++, .Net and Python. Here we illustrate using it from Python. This function lets a client register a callback that is invoked whenever the main SMT search engine makes an inference. The main inference steps are (1) introducing an assumption, that is, a clause that is entailed by the input formula (formally, the clause is satisfiability preserving when added to the input formula), (2) deleting a clause from the set of active clauses, (3) rup, inferring a clause through propositional reasoning, the clause can be justified using reverse unit propagation, (4) smt, or other theory rule, when a clause is added as a theory tautology.

Capture just quantifier instantiations

Applications that only need to monitor quantifier instantiations can filter the stream based on the name of the proof hint.

Monitor clauses annotated with detailed justifications

If you set proof mode to true, then the inferred clauses are annotated by more detailed proof terms. The detailed proof terms use a repertoire or low level inference rules.

Monitor proof objects from the new core

An SMT core based on Z3's better tuned SAT solver is accessible if you set the option sat.euf=true and force it by setting tactic.default_tactic=sat (or to smt). Proof logs have a different format from the legacy core. Many inferences in this format can be checked efficiently by built-in validators.

Saving and restoring inferences to and from files

Inference logging, replay, and checking is supported for the core enabled by setting sat.euf = true. setting the default tactic to 'sat' bypasses other tactics that could end up using different solvers.

Parse the logged inferences and replay them

You can also replay inferences when parsing a previously saved inferences.

This feature can be used when proof logs are produced using a command-line process. Log reconstruction is decoupled form the shell process.

Parse the logged inferences and check them

Now turn on proof checking. It invokes the self-validator. The self-validator produces log lines of the form:

   (proofs +tseitin 60 +alldiff 8 +euf 3 +rup 5 +inst 6 -quant 3 -inst 2)
(verified-smt
(inst (forall (vars (x T) (y T) (z T)) (or (subtype (:var 2) (:var 1)) ...

The 'proofs' line summarizes inferences that were self-validated. The pair +tseitin 60 indicates that 60 inferences were validated as Tseitin encodings. The pair -inst 2 indicates that two quantifier instantiations were not self-validated They were instead validated using a call to SMT solving. A log for an smt invocation is exemplified in the next line. Note that the pair +inst 6 indicates that 6 quantifier instantiations were validated using a syntactic (cheap) check. Some quantifier instantiations based on quantifier elimination are not simple substitutions and therefore a simple syntactic check does not suffice.

Verify and self-validate on the fly

Proof checking (self-validation) is on by default. In case someone turned it off we force it to be on in the following.

Verify and self-validate on the fly, but don't check rup

Self-validation checks all inferences. Inferences that use theory lemmas can be checked locally without considering other inferences. Inferences that are annotated by rup (reverse unit propagation) are justified using global inferences (unit propagation). Checking rup during search can be very expensive. You can turn off rup checking selectively to speed up validation for theory lemmas.

Command line uses

From the command-line you can enable self-validation using the parameters

   z3 <file.smt2> sat.euf=true tactic.default_tactic=smt solver.proof.check=true

You can disable rup checking

   z3 <file.smt2> sat.euf=true tactic.default_tactic=smt solver.proof.check=true sat.smt.proof.check_rup=false

To save proof logs, but not check them, use

z3 <file.smt2> sat.euf=true tactic.default_tactic=smt solver.proof.log=<logfile.smt2>

Inferences

Inferences are printed in a mild extension of SMTLIB2. The extension has three new commands

(infer clause proof_hint)
(del clause)
(assume clause)

where a proof hint is a proof term that is either a detailed set of inference steps or a generic inference that requires a proof checker that understands more than a set of simple syntactic inferences.

Proof Hints

The format of proof hints is set up so it can be extended when new features are added. Common to proof hints is that every proof hint encodes a claim in the form of a clause. A proof hint should justify the clause. We defer a detailed documentation of proof hints but summarize some of the main hints in use:

  • tseitin - The claim is justified by a Tseitin transformation.
  • euf - The claim follows from equality reasoning.
  • inst - A quantifier is instantiated using a binding given in the hint.
  • farkas - The negation of the claim is a conjunction of inequalities. The farkas hint contains coefficients such that the inequalities, when added modulo multiplying with coefficients, sum up to a tight and inconsistent inequality.
  • bound - An inequality is derived using a combination of inequalities and cuts
  • implied-eq - An implied inequality can be derived from a set of inequalities and equalities.

A sample session with proof logs

We have the following formula

(set-option :sat.euf true)
(set-option :tactic.default_tactic smt)
(set-option :solver.proof.log proof_log.smt2)
(declare-fun d (Int Int) Int)
(declare-fun t (Int Int Real) (Array Int (Array Int Real)))
(assert (forall ((u Int) (v Real)) (= v (select (select (t v (d 1 0) (d 0 u)) 0) 0))))
(check-sat)

It produces a proof log of the form

(declare-fun t (Int Int Real) (Array Int (Array Int Real)))
(declare-fun d (Int Int) Int)
(define-const $33 Bool (forall ((u Int) (v Real))
(let ((a!1 (select (t (to_int v) (d 1 0) (to_real (d 0 u))) 0)))
(= v (select a!1 0)))))
(assume $33)
(assume true)
(define-const $63 Int (d 0 0))
(define-const $64 Real (to_real $63))
(define-const $25 Int (d 1 0))
(define-const $72 (Array Int (Array Int Real)) (t (- 1) $25 $64))
(define-const $73 (Array Int Real) (select $72 0))
(define-const $74 Real (select $73 0))
(define-const $75 Bool (= (- 1.0) $74))
(declare-fun inst (Bool Bool Proof) Proof)
(declare-fun bind (Int Real) Proof)
(define-const $61 Proof (bind 0 (- 1.0)))
(define-const $76 Bool (not $75))
(define-const $65 Proof (inst $33 $76 $61))
(infer $75 $65)
(define-const $105 Int (d 0 $63))
(define-const $106 Real (to_real $105))
(define-const $113 (Array Int (Array Int Real)) (t (- 1) $25 $106))
(define-const $114 (Array Int Real) (select $113 0))
(define-const $115 Real (select $114 0))
(define-const $116 Bool (= (- (/ 1.0 2.0)) $115))
(define-const $109 Proof (bind $63 (- (/ 1.0 2.0))))
(define-const $117 Bool (not $116))
(define-const $57 Proof (inst $33 $117 $109))
(infer $116 $57)
(define-const $123 Bool (= (- (/ 3.0 4.0)) $115))
(define-const $110 Proof (bind $63 (- (/ 3.0 4.0))))
(define-const $124 Bool (not $123))
(define-const $102 Proof (inst $33 $124 $110))
(infer $123 $102)
(declare-fun euf (Bool Bool) Proof)
(define-const $41 Proof (euf $123 $116))
(infer $41)
(declare-fun rup () Proof)
(infer rup)

The log is mildly speaking not human readable. But you can use scripts to read the log.

from z3 import *
set_param("solver.proof.check", False)
s = Solver()
onc = OnClause(s, print)
s.from_file("proof_log.smt2")

The result is easier to digest

assumption [ForAll([u, v],
v == t(ToInt(v), d(1, 0), ToReal(d(0, u)))[0][0])]
assumption [True]
inst(ForAll([u, v],
v == t(ToInt(v), d(1, 0), ToReal(d(0, u)))[0][0]),
Not(-1 == t(-1, d(1, 0), ToReal(d(0, 0)))[0][0]),
bind(0, -1)) [-1 == t(-1, d(1, 0), ToReal(d(0, 0)))[0][0]]
inst(ForAll([u, v],
v == t(ToInt(v), d(1, 0), ToReal(d(0, u)))[0][0]),
Not(-(1/2) ==
t(-1, d(1, 0), ToReal(d(0, d(0, 0))))[0][0]),
bind(d(0, 0), -(1/2))) [-(1/2) == t(-1, d(1, 0), ToReal(d(0, d(0, 0))))[0][0]]
inst(ForAll([u, v],
v == t(ToInt(v), d(1, 0), ToReal(d(0, u)))[0][0]),
Not(-(3/4) ==
t(-1, d(1, 0), ToReal(d(0, d(0, 0))))[0][0]),
bind(d(0, 0), -(3/4))) [-(3/4) == t(-1, d(1, 0), ToReal(d(0, d(0, 0))))[0][0]]
euf(-(3/4) == t(-1, d(1, 0), ToReal(d(0, d(0, 0))))[0][0],
-(1/2) == t(-1, d(1, 0), ToReal(d(0, d(0, 0))))[0][0]) []
rup []

SMT proofs are of course generally much larger.